Dart_Algolia Lazy Loading

施工中……

Dart_Algolia%20Lazy%20Loading%20ae60545013ab4124aafbdb701a3208e1/Untitled.png

6.6 滚动监听及控制

Lazy Loading例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class MyHome extends StatefulWidget {
@override
_MyHomeState createState() => new _MyHomeState();
}

class _MyHomeState extends State<MyHome> {
ScrollController controller;
List<String> items = new List.generate(100, (index) => 'Hello $index');

@override
void initState() {
super.initState();
controller = new ScrollController()..addListener(_scrollListener);
}

@override
void dispose() {
controller.removeListener(_scrollListener);
super.dispose();
}

@override
Widget build(BuildContext context) {
return new Scaffold(
body: new Scrollbar(
child: new ListView.builder(
controller: controller,
itemBuilder: (context, index) {
return new Text(items[index]);
},
itemCount: items.length,
),
),
);
}

void _scrollListener() {
print(controller.position.extentAfter);
if (controller.position.extentAfter < 500) {
setState(() {
items.addAll(new List.generate(42, (index) => 'Inserted $index'));
});
}
}
}

Flutter ListView lazy loading

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bool _handleScrollNotification(ScrollNotification notification) {
if (notification is ScrollEndNotification) {
if (_controller.position.extentAfter == 0) {
loadMore();
}
}
return false;
}

@override
Widget build(BuildContext context) {
final Widget gridWithScrollNotification = NotificationListener<
ScrollNotification>(
onNotification: _handleScrollNotification,
child: GridView.count(
controller: _controller,
padding: EdgeInsets.all(4.0),
// Create a grid with 2 columns. If you change the scrollDirection to
// horizontal, this would produce 2 rows.
crossAxisCount: 2,
crossAxisSpacing: 2.0,
mainAxisSpacing: 2.0,
// Generate 100 Widgets that display their index in the List
children: _documents.map((doc) {
return GridPhotoItem(
doc: doc,
);
}).toList()));
return new Scaffold(
key: _scaffoldKey,
body: RefreshIndicator(
onRefresh: _handleRefresh, child: gridWithScrollNotification));
}

同样摘自上面的链接,里面还有个有意思的组件,也可以用来Lazy Loading

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
List<List<String>> lists = [["A1","A2","A3","A4","A5"],["B1","B2","B3"],["C1","C2","C3","C4","C5"],["D1","D2","D3"]];

Widget buildCard(String text) {
return Container(
margin: EdgeInsets.all(4.0),
padding: EdgeInsets.all(40.0),
alignment: Alignment.center,
color: Colors.lightGreenAccent,
child: Text(text),
);
}

Widget buildHorizontalList(List<String> sublist) {
return SizedBox(
height: 200.0,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: sublist.length,
itemBuilder: (context, index) => buildCard("${sublist[index]}"),
),
);
}

Widget buildVerticalList(List<String> sublist) {
return ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: sublist.length,
itemBuilder: (context, index) {
return buildCard("${sublist[index]}");
},
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: lists.length + 1,
itemBuilder: (context, index) {
if (index <= lists.length - 1) {
return index.isEven ? buildHorizontalList(lists[index]) : buildVerticalList(lists[index]);
}
return RaisedButton(
child: Text('Load More'),
onPressed: () {
setState(() {
lists.addAll([["X1","X2","X3","X4","X5"],["Y1","Y2","Y3"]]);
});
},
);
}),
);
}

这个要使用者点击按钮来加载数据,也很有意思。

How to load multiple list view inside single list view?

突然想到google教程里的一个例子,这个例子比较特殊,没有用到setState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
class _RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = Set<WordPair>();
final _biggerFont = TextStyle(fontSize: 18.0);
@override
Widget build(BuildContext context) {
//final size = MediaQuery.of(context).size.width;
return Scaffold(
appBar: AppBar(
title: Text('Startup Name Generator'),
actions: [
IconButton(icon: Icon(Icons.list), onPressed: _pushSaved),
],
),
body: _buildSuggestions(),
);
}

void _pushSaved() {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
final tiles = _saved.map(
(WordPair pair) {
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();

return Scaffold(
appBar: AppBar(
title: Text('Saved Suggestions'),
),
body: ListView(children: divided),
);
},
),
);
}

Widget _buildSuggestions() {
return ListView.builder(
//没有itemCount,所以用不着SetState
padding: EdgeInsets.all(16.0),
itemBuilder: (context,i){
if(i.isOdd) return Divider();

final index = i ~/2;
if(index >= _suggestions.length){
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
}
);
}

Widget _buildRow(WordPair pair){
final alreadySaved = _saved.contains(pair);
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: Icon(
alreadySaved ?Icons.favorite:Icons.favorite_border,
color: alreadySaved ? Colors.red:null,
),
onTap: (){
setState(() {
if(alreadySaved){
_saved.remove(pair);
}else{
_saved.add(pair);
}
});
},
);
}
}

毕设相关的话:

为啥要改成Lazy Loading呢?本来都不打算搞这玩意儿的,但是Algolia API限制每次query返回的结果数,这使得我不得不重写代码。(API 有hitsPerPage【returns the maximum number of hits returned for each page】这玩意儿我还是在代码报错后才发现的,在查出错误原因前我还一直纠缠于TextField的焦点与ListView的焦点问题,以为是listview的bug,昨晚睡前debug才发现listview报的RangeError (index): Invalid value: Not in inclusive range 0..19: 20 是真的超了,algolia返回匹配结果数是21,每次默认只传20个……以后还是得认真看看API文档)

下面,贴文档:

  1. Pagination
  2. Infinite Scroll

第二个链接里有js的lazy loading写法,可以借鉴:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let lastRenderArgs;

const infiniteHits = instantsearch.connectors.connectInfiniteHits(
(renderArgs, isFirstRender) => {
const { hits, showMore, widgetParams } = renderArgs;
const { container } = widgetParams;

lastRenderArgs = renderArgs;

if (isFirstRender) {
const sentinel = document.createElement('div');
container.appendChild(document.createElement('ul'));
container.appendChild(sentinel);

const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting && !lastRenderArgs.isLastPage) {
showMore();
}
});
});

observer.observe(sentinel);

return;
}

// ...
}
);

SearchDelegate相关

因为SearchDelegate默认不支持State,上面代码中设置了itemCount的ListView所用的SetState显然不能直接塞进我们的代码中,这里有两种解决方法:

1. 改flutter search.dart源码

Allow search page to maintain state when navigating to a new route.#45263

这个PR本来打算直接使SearchDelegate支持State,可惜的是提交PR的人后来改主意了,真是可惜,不过我们还是可以自己改源码来实现该功能

2. StatefulBuilder,自己建立状态子树

how to use setState in SearchDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DataSearch extends SearchDelegate<String> {
bool _isItemSelected = true;

//...rest of the @override methods

@override
Widget buildSuggestions(BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return CheckboxListTile(
title: const Text('Item'),
value: _isItemSelected,
onChanged: (bool newValue) {
setState(() {
_isItemSelected = newValue;
});
},
);
});
}
}

其他知识

滚动监听

ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件,如:

1
controller.addListener(()=>print(controller.offset))

ScrollController使用方法

  1. Focus the bottom of a ListView ? Flutter
  2. 【Flutter】可滚动组件之滚动控制和监听

焦点相关 (这边没看懂,也没用的上)

  1. 焦点和文本框
  2. Flutter - Focusable ListView items